/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.eclipse.andmore.android.certmanager.ui.model;

import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStore.Entry;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Set;

import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.eclipse.andmore.android.certmanager.CertificateManagerActivator;
import org.eclipse.andmore.android.certmanager.core.KeyStoreUtils;
import org.eclipse.andmore.android.certmanager.core.PasswordProvider;
import org.eclipse.andmore.android.certmanager.exception.KeyStoreManagerException;
import org.eclipse.andmore.android.certmanager.i18n.CertificateManagerNLS;
import org.eclipse.andmore.android.certmanager.views.KeystoreManagerView;
import org.eclipse.andmore.android.common.log.AndmoreLogger;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.osgi.util.NLS;
import org.eclipse.ui.plugin.AbstractUIPlugin;

/**
 * Represents one keystore element in {@link KeystoreManagerView}. It can be
 * {@link Certificate} or {@link Key}.
 */
public class EntryNode extends AbstractTreeNode implements IKeyStoreEntry {
	/**
	 * The constant contains the key pair DER object identifier.
	 */
	public static final String KEY_PAIR_DER_OBJ_ID = "2.16.840.1.113793.23"; //$NON-NLS-1$    

	public static final int KEY_PASSWORD_MIN_SIZE = 6;

	protected String alias;

	private final String KEY_NONSAVED_PASSWORD_ICON_PATH = "icons/key.png"; //$NON-NLS-1$

	private final String KEY_SAVED_PASSWORD_ICON_PATH = "icons/key_saved_password.png"; //$NON-NLS-1$

	protected EntryNode() {

	}

	/**
	 * 
	 * @param keyStoreModel
	 * @param alias
	 * @throws KeyStoreManagerException
	 *             if the alias is already listed in the tree
	 */
	public EntryNode(ITreeNode keyStoreModel, String alias) throws KeyStoreManagerException {
		this.alias = alias.toLowerCase();
		setParent(keyStoreModel);
		if (!isKeyPairEntry()) {
			keyStoreModel.addChild(this);
		}

		// notify key entry addition
		// KeyStoreModelEventManager.getInstance().fireEvent(this,
		// KeyStoreModelEvent.EventType.ADD);

		// Obtaining certificate to get tooltip information
		X509Certificate cert = getX509Certificate();
		if (cert != null) {
			X500Name x500name;
			try {
				x500name = new JcaX509CertificateHolder(cert).getSubject();

				RDN commonName = x500name.getRDNs(BCStyle.CN).length >= 1 ? x500name.getRDNs(BCStyle.CN)[0] : null;
				RDN organization = x500name.getRDNs(BCStyle.O).length >= 1 ? x500name.getRDNs(BCStyle.O)[0] : null;

				// Adding tooltip information
				String org = organization != null ? organization.getFirst().getValue().toString()
						: CertificateManagerNLS.CertificateInfoDialog_NotAvailableProperty;
				String name = commonName != null ? commonName.getFirst().getValue().toString()
						: CertificateManagerNLS.CertificateInfoDialog_NotAvailableProperty;
				this.setTooltip(NLS.bind(CertificateManagerNLS.CertificateBlock_KeyTooltip, org, name));
			} catch (CertificateEncodingException e) {
				String errorMsg = "Error getting data from certificate";
				AndmoreLogger.error(EntryNode.class, errorMsg, e);
				throw new KeyStoreManagerException(errorMsg, e);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.andmore.android.certmanager.ui.model.IKeyStoreEntry
	 * #getKeyStoreNode()
	 */
	@Override
	public IKeyStore getKeyStoreNode() {
		return (KeyStoreNode) getParent();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.andmore.android.certmanager.ui.model.IKeyStoreEntry
	 * #getAlias()
	 */
	@Override
	public String getAlias() {
		return alias;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.andmore.android.certmanager.ui.model.IKeyStoreEntry
	 * #isCertificateEntry()
	 */
	@Override
	public boolean isCertificateEntry() throws KeyStoreException, KeyStoreManagerException {
		return getKeyStoreNode().getKeyStore().isCertificateEntry(alias);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.andmore.android.certmanager.ui.model.IKeyStoreEntry
	 * #isKeyEntry()
	 */
	@Override
	public boolean isKeyEntry() throws KeyStoreException, KeyStoreManagerException {
		return getKeyStoreNode().getKeyStore().isKeyEntry(alias);
	}

	@Override
	public boolean isKeyPairEntry() {
		X509Certificate certificate = getX509Certificate();
		Set<String> criticalOIDs = certificate.getCriticalExtensionOIDs();
		return (criticalOIDs != null) && criticalOIDs.contains(KEY_PAIR_DER_OBJ_ID);
	}

	/**
	 * @return {@link Certificate} if alias represents a certificate or null if
	 *         the alias was not found (or if the type is not Certificate for
	 *         the alias)
	 * @throws KeyStoreException
	 *             if keystore not loaded yet
	 * @throws KeyStoreManagerException
	 */
	private Certificate getCertificate() throws KeyStoreException, KeyStoreManagerException {
		Certificate certificate = null;
		KeyStore keyStore = getKeyStoreNode().getKeyStore();
		if (keyStore.isCertificateEntry(alias)) {
			certificate = keyStore.getCertificate(alias);
		} else {
			// unknown type
			AndmoreLogger.error(NLS.bind(CertificateManagerNLS.EntryNode_NotFoundOrTypeWrong, alias));
		}
		return certificate;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.andmore.android.certmanager.ui.model.IKeyStoreEntry
	 * #getKey(java.lang.String)
	 */
	@Override
	public Key getKey(String password) throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException,
			KeyStoreManagerException {
		Key key = null;
		KeyStore keyStore = getKeyStoreNode().getKeyStore();
		if (keyStore.isKeyEntry(alias)) {
			key = keyStore.getKey(alias, password.toCharArray());
		}

		return key;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.andmore.android.certmanager.ui.model.IKeyStoreEntry
	 * #getPrivateKey(java.lang.String)
	 */
	@Override
	public PrivateKey getPrivateKey(String password) throws UnrecoverableKeyException, KeyStoreException,
			NoSuchAlgorithmException, KeyStoreManagerException, InvalidKeyException {
		Key key = this.getKey(password);

		if (!(key instanceof PrivateKey)) {
			throw new InvalidKeyException("This is not a private key");
		}

		return (PrivateKey) key;
	}

	public Entry getKeyEntry(String password) throws KeyStoreException, NoSuchAlgorithmException,
			KeyStoreManagerException, UnrecoverableEntryException {
		Entry key = null;
		KeyStore keyStore = getKeyStoreNode().getKeyStore();
		if (keyStore.isKeyEntry(alias)) {
			key = keyStore.getEntry(alias, new KeyStore.PasswordProtection(password.toCharArray()));
		}
		return key;
	}

	/**
	 * Get all the certificates associated to this entry
	 * 
	 * @return an Array of {@link Certificate}
	 * @throws KeyStoreException
	 * @throws KeyStoreManagerException
	 */
	private Certificate[] getCertificateChain() throws KeyStoreException, KeyStoreManagerException {
		KeyStore keyStore = getKeyStoreNode().getKeyStore();
		return keyStore.getCertificateChain(alias);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = (prime * result) + ((alias == null) ? 0 : alias.hashCode());
		result = (prime * result) + ((getKeyStoreNode() == null) ? 0 : getKeyStoreNode().hashCode());
		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj == null) {
			return false;
		}
		if (!(obj instanceof EntryNode)) {
			return false;
		}
		EntryNode other = (EntryNode) obj;
		if (alias == null) {
			if (other.alias != null) {
				return false;
			}
		} else if (!alias.equals(other.alias)) {
			return false;
		}
		if (getKeyStoreNode() == null) {
			if (other.getKeyStoreNode() != null) {
				return false;
			}
		} else if (!getKeyStoreNode().equals(other.getKeyStoreNode())) {
			return false;
		}
		return true;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "KeyStoreEntry [alias=" + alias + "]"; //$NON-NLS-1$ //$NON-NLS-2$
	}

	@Override
	public void refresh() {
		// keys does not need to be refreshed
	}

	@Override
	public String getId() {
		return alias;
	}

	@Override
	public String getName() {
		return alias;
	}

	@Override
	public ImageDescriptor getIcon() {
		// decision: we will not support key-pair, so we will have just key
		// items below keystore node.
		ImageDescriptor descr = null;
		if (isPasswordSaved()) {
			// saved password
			descr = AbstractUIPlugin.imageDescriptorFromPlugin(CertificateManagerActivator.PLUGIN_ID,
					KEY_SAVED_PASSWORD_ICON_PATH);
		} else {
			// non saved password
			descr = AbstractUIPlugin.imageDescriptorFromPlugin(CertificateManagerActivator.PLUGIN_ID,
					KEY_NONSAVED_PASSWORD_ICON_PATH);
		}
		return descr;
	}

	@Override
	public boolean isLeaf() {
		return true;
	}

	@Override
	public List<ITreeNode> getChildren() {
		return new ArrayList<ITreeNode>(0); // it is the leaf of the tree
	}

	public static IKeyStoreEntry createSelfSignedNode(IKeyStore keystore, String keyStorePass, String alias,
			CertificateDetailsInfo certificateDetailsInfo) throws KeyStoreManagerException {
		KeyPair keyPair = null;

		try {
			keyPair = KeyStoreUtils.genKeyPair();
			X509Certificate x509Certificate = KeyStoreUtils.createX509Certificate(keyPair, certificateDetailsInfo);

			if (keyStorePass == null) {
				PasswordProvider provider = new PasswordProvider(keystore.getFile());
				keyStorePass = provider.getKeyStorePassword(true);
			}

			PrivateKeyEntry privateKeyEntry = KeyStoreUtils.createPrivateKeyEntry(keyPair, x509Certificate);
			KeyStoreUtils.addEntry(keystore.getKeyStore(), keyStorePass.toCharArray(), keystore.getFile(), alias,
					privateKeyEntry, certificateDetailsInfo.getEntryPassword().toCharArray());

			// force reload - because keystore cache can be old due to key
			// entries additions/removals
			keystore.forceReload(keyStorePass.toCharArray(), false);
		} catch (Exception e) {
			throw new KeyStoreManagerException(e.getMessage(), e);
		}

		return new EntryNode((ITreeNode) keystore, alias);
	}

	public static IKeyStoreEntry createSelfSignedNode(IKeyStore keystore, String alias,
			CertificateDetailsInfo certificateDetailsInfo) throws KeyStoreManagerException {
		return createSelfSignedNode(keystore, null, alias, certificateDetailsInfo);
	}

	@Override
	public boolean testAttribute(Object target, String name, String value) {
		boolean result = super.testAttribute(target, name, value);
		if (name.equals(PROP_NAME_NODE_STATUS)) {
			if (value.equals(PROP_VALUE_NODE_STATUS_WARNING)) {
				X509Certificate x509Certificate = getX509Certificate();
				try {
					// check validity concerning the current date
					x509Certificate.checkValidity();

					// now check validity related to magic date provided by
					// Google
					Calendar date = Calendar.getInstance();
					date.clear();
					date.set(2033, Calendar.OCTOBER, 22);
					x509Certificate.checkValidity(date.getTime());
				} catch (CertificateExpiredException e) {
					// certificate has expired in the current date; or
					// certificate has expired before 22 Oct 2033
					setTooltip(NLS.bind(CertificateManagerNLS.CertificatePeriodExpired_Issue,
							x509Certificate.getNotAfter()));
					result = true; // decorate node
				} catch (CertificateNotYetValidException e) {
					// certificate is not yet valid in the current date; or
					// certificate is not yet valid in 2033 => it must not
					// happen but we need to deal with this case
					setTooltip(NLS.bind(CertificateManagerNLS.CertificatePeriodNotYeatValid_Issue,
							x509Certificate.getNotBefore()));
					result = true; // decorate node
				}
			}
		}
		return result;

	}

	/**
	 * Get the first X509Certificate available in the entry
	 * 
	 * @return
	 */
	@Override
	public X509Certificate getX509Certificate() {
		X509Certificate x509Certificate = null;
		try {
			if (isCertificateEntry()) {
				Certificate cert = getCertificate();
				if (cert instanceof X509Certificate) {
					// Android certificate
					x509Certificate = (X509Certificate) cert;
				}
			} else if (isKeyEntry()) {
				Certificate[] chain = getCertificateChain();
				for (int i = 0; i < chain.length; i++) {
					Certificate cert = chain[i];
					if (cert instanceof X509Certificate) {
						// Android certificate
						x509Certificate = (X509Certificate) cert;
					}
				}
			}
		} catch (Exception e) {
			AndmoreLogger.error(EntryNode.class,
					NLS.bind(CertificateManagerNLS.EntryNode_ErrorGettingCertificateFromEntry, getAlias()), e);
		}
		return x509Certificate;
	}

	@Override
	protected boolean isPasswordSaved() {
		PasswordProvider pp = new PasswordProvider(getKeyStoreNode().getFile());
		return pp.isPasswordSaved(alias);
	}
}
